/*
 * Copyright (C) 2009 Tony Gentilcore
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.scoreninja;

import java.io.IOException;
import java.text.DateFormat;
import java.util.Date;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.Typeface;
import android.location.Address;
import android.location.Geocoder;
import android.location.Location;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Message;
import android.os.Handler;
import android.text.Spannable;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.View.OnClickListener;
import android.widget.EditText;
import android.widget.TableLayout;
import android.widget.TableRow;
import android.widget.TextView;

/**
 * Displays the score board.
 * 
 * @author tonygentilcore@gmail.com (Tony Gentilcore)
 */
public final class ScoreNinja extends Activity {
  static final String PREFS_NAME = "ScoreNinjaPrefs";
  static final String MY_SILLY_ESCAPE_STRING = "&&q;;";
  
  // Intent and Bundle keys.
  static final String KEY_APP_ID = "appid";
  static final String KEY_NEW_SCORE = "newscore";
  static final String KEY_NEW_PLACE = "newplace";
  static final String KEY_SIGNATURE = "signature";
  static final String KEY_COMMENTS = "comments";
  static final String KEY_NAME = "name";
  static final String KEY_LOCATION = "location";
  static final String KEY_TITLE = "title";
  static final String KEY_SUB_BOARD = "subboard";
  static final String KEY_RESPONSE = "response";
  static final String KEY_ORIENTATION = "orientation";
  static final String KEY_SHOWED_DIALOG = "showeddialog";

  // JSON keys.
  private static final String JSON_COUNT = "cnt";
  private static final String JSON_LIMIT = "lmt";
  private static final String JSON_ALLTIME = "all";
  private static final String JSON_PERCENTILES = "per";
  private static final String JSON_SCORE = "scr";
  private static final String JSON_NAME = "nam";
  private static final String JSON_LOCATION = "loc";
  private static final String JSON_COMMENTS = "com";
  private static final String JSON_CREATED = "cre";
  
  private LocationManager locator;
  private Geocoder geocoder;
  
  private String appId;
  private Resources resources;
  private DateFormat systemDateFormat;

  private String scoreSignature;
  private int newScore = 0;
  private int newPlace = 0;
  private boolean showedDialog = false;
  private String name;
  private String comments;
  private String subBoard;
  private String titleText;
  private int orientation;
  private volatile String response;
  private volatile JSONArray scores;
  private volatile JSONArray percentiles;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    if (savedInstanceState != null) {
      appId = savedInstanceState.getString(KEY_APP_ID);
      newScore = savedInstanceState.getInt(KEY_NEW_SCORE);
      newPlace = savedInstanceState.getInt(KEY_NEW_PLACE);
      showedDialog = savedInstanceState.getBoolean(KEY_SHOWED_DIALOG);
      scoreSignature = savedInstanceState.getString(KEY_SIGNATURE);
      name = savedInstanceState.getString(KEY_NAME);
      comments = savedInstanceState.getString(KEY_COMMENTS);
      subBoard = savedInstanceState.getString(KEY_SUB_BOARD);
      titleText = savedInstanceState.getString(KEY_TITLE);
      orientation = savedInstanceState.getInt(KEY_ORIENTATION);
    }

    Intent intent = getIntent();
    
    // Get the appId from the intent then the bundle.
    if (intent.hasExtra(KEY_APP_ID)) {
      appId = intent.getStringExtra(KEY_APP_ID);
    }
    
    // Get the newScore from the intent then the bundle.
    if (intent.hasExtra(KEY_NEW_SCORE)) {
      newScore = intent.getIntExtra(KEY_NEW_SCORE, 0);
      scoreSignature = intent.getStringExtra(KEY_SIGNATURE);
    }
    
    if (intent.hasExtra(KEY_SUB_BOARD)) subBoard = intent.getStringExtra(KEY_SUB_BOARD);
    if (intent.hasExtra(KEY_TITLE)) titleText = intent.getStringExtra(KEY_TITLE);
    if (intent.hasExtra(KEY_ORIENTATION)) orientation = intent.getIntExtra(KEY_ORIENTATION, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);

    // Can't launch without an appid.
    if (appId == null) {
      finish();
      return;
    }
    
    // Build the UI.
    if (orientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) setRequestedOrientation(orientation);
    requestWindowFeature(Window.FEATURE_NO_TITLE);
    setContentView(R.layout.scoreninja_layout);
    systemDateFormat = DateFormat.getDateInstance(DateFormat.SHORT);
    resources = getResources();
    findViewById(R.id.subwindow_close).setOnClickListener(new OnClickListener() {
      public void onClick(View v) {
        finish();
      }   
    });
    if (titleText != null) {
      ((TextView)findViewById(R.id.title)).setText(titleText);
    }
    
    locator = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
    geocoder = new Geocoder(this);
    
    // After the UI is built, check if we have a cached response.
    if (savedInstanceState != null) {
      if (setResponse(savedInstanceState.getString(KEY_RESPONSE), false)) {
        return;
      }
    }

    SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0);
    if (settings.contains(getResponsePrefKey(appId, subBoard))) {
      if (setResponse(getResponseFromPrefs(appId, subBoard, settings), false)) {
        return;
      }
    }
    
    setProgressBarVisibility(true);
    ScoreNinjaService.requestBoard(appId, subBoard, new Handler() {
      @Override
      public void handleMessage(Message msg) {
        String response = msg.getData().getString(ScoreNinja.KEY_RESPONSE);
        if (setResponse(response, true)) {
          commitResponseToPrefs(appId, subBoard, getSharedPreferences(PREFS_NAME, 0),
                  response);
        }
      }
    });    
  }

  @Override
  protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent);
    appId = intent.getStringExtra(KEY_APP_ID);
    // Can't launch without an appid.
    if (appId == null) {
      finish();
      return;
    }
    newScore = intent.getIntExtra(KEY_NEW_SCORE, 0);
    scoreSignature = intent.getStringExtra(KEY_SIGNATURE);
    subBoard = intent.getStringExtra(KEY_SUB_BOARD);
    titleText = intent.getStringExtra(KEY_TITLE);
    orientation = intent.getIntExtra(KEY_ORIENTATION, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
    if (orientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) setRequestedOrientation(orientation);
  }
  
  /**
   * Notification that something is about to happen, to give the Activity a
   * chance to save state.
   * 
   * @param outState a Bundle into which this Activity should save its state
   */
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    if (outState != null) {
      outState.putString(KEY_APP_ID, appId);
      outState.putString(KEY_RESPONSE, response);
      outState.putString(KEY_SIGNATURE, scoreSignature);
      outState.putString(KEY_NAME, name);
      outState.putString(KEY_COMMENTS, comments);
      outState.putString(KEY_TITLE, titleText);
      outState.putString(KEY_SUB_BOARD, subBoard);
      outState.putInt(KEY_NEW_SCORE, Integer.valueOf(newScore));
      outState.putInt(KEY_NEW_PLACE, Integer.valueOf(newPlace));
      outState.putInt(KEY_ORIENTATION, Integer.valueOf(orientation));
      outState.putBoolean(KEY_SHOWED_DIALOG, Boolean.valueOf(showedDialog));
    }
  }

  /**
   * Parses a JSON response and updates the table view.
   * 
   * @param response
   * @return true iff the response was successfully parsed.
   */
  private boolean setResponse(String response, boolean updateUiOnFail) {
    boolean success = true;
    if (response == null) {
      return false;
    }
    if (!response.equals(this.response)) {
      try {
        JSONObject json = new JSONObject(response);
        scores = json.getJSONArray(JSON_ALLTIME);
        percentiles = json.getJSONArray(JSON_PERCENTILES);
        this.response = response;
      } catch (JSONException e) {
        if (!updateUiOnFail) {
          return false;
        }
        success = false;
      }
      updateTable();
      setScoreResult();
    }
    return success;
  }

  /**
   * Calculates the percentile for the given score.
   * 
   * @param score
   * @return
   */
  private double getPercentile(int score) {
    try {
      JSONObject percentile = percentiles.getJSONObject(0);
      int totalCount = percentile.getInt(JSON_COUNT);
      for (int i = 0; i < percentiles.length() - 1; i++) {
        percentile = percentiles.getJSONObject(i);
        int upperLimit = percentile.getInt(JSON_LIMIT);
        int count = percentile.getInt(JSON_COUNT);
        JSONObject nextPercentile = percentiles.getJSONObject(i + 1);
        int nextUpperLimit = nextPercentile.getInt(JSON_LIMIT);
        if (score <= upperLimit && score >= nextUpperLimit) {
          int nextCount = nextPercentile.getInt(JSON_COUNT);
          float fraction = (score - nextUpperLimit)
                  / (float) (upperLimit - nextUpperLimit);
          int newCount = nextCount + (int) (fraction * (count - nextCount));
          if (newCount > 0) {
            return 100 * newCount / totalCount;
          }
          return 0;
        }
      }
      percentile = percentiles.getJSONObject(percentiles.length() - 1);
      int upperLimit = percentile.getInt(JSON_LIMIT);
      int count = percentile.getInt(JSON_COUNT);
      float fraction = score / (float) (upperLimit);
      return 100 * (fraction * count) / totalCount;
    } catch (JSONException e) {
    }
    return 0;
  }
  
  static void commitResponseToPrefs(
          String appId,
          String subBoard,
          SharedPreferences prefs,
          String response) {
    SharedPreferences.Editor editor = prefs.edit();
    editor.putString(getResponsePrefKey(appId, subBoard), response.replaceAll("\"", ScoreNinja.MY_SILLY_ESCAPE_STRING));
    editor.commit();
  }

  /**
   * Hacks around an annoying bug by replacing all occurrences of two or more double quotes with a single double quote.
   * This can be removed once this bug is resolved: http://code.google.com/p/android/issues/detail?id=1707
   * @param settings
   * @return
   */
  static String getResponseFromPrefs(String appId, String subBoard, SharedPreferences settings) {
    String resp = settings.getString(getResponsePrefKey(appId, subBoard), null);
    if (resp != null) {
      resp = resp.replaceAll(MY_SILLY_ESCAPE_STRING, "\"");
    }
    return resp;
  }
  
  static String getResponsePrefKey(String appId, String subBoard) {
    return KEY_RESPONSE + "_" + appId + (subBoard != null ? ("_" + subBoard) : "");
  }

  @Override
  protected Dialog onCreateDialog(int id) {
    LayoutInflater factory = LayoutInflater.from(this);
    final View textEntryView = factory
            .inflate(R.layout.scoreninja_prompt, null);
    return new AlertDialog.Builder(this).setIcon(
            android.R.drawable.btn_star_big_off).setView(textEntryView)
            .setTitle("You got the #" + newPlace + " score")
            .setPositiveButton(resources.getString(R.string.submit_button),
                    new DialogInterface.OnClickListener() {
                      public void onClick(DialogInterface dialog,
                              int whichButton) {
                        // User clicked submit
                        name = ((EditText) textEntryView
                                .findViewById(R.id.username_edit)).getText()
                                .toString();
                        comments = ((EditText) textEntryView
                                .findViewById(R.id.comments_edit)).getText()
                                .toString();
                        setScoreResult();
                        updateTableWithNewHighScore();
                      }
                    }).setNegativeButton(
                    resources.getString(R.string.cancel_button), null).create();
  }

  /**
   * Sets the result of this activity to tell the calling activity that a new
   * high score was achieved. The caller will then sign the score and call this
   * activity again to submit it.
   */
  private void setScoreResult() {
    // Submit the new high score.
    if (newScore < 1) {
      setResult(RESULT_CANCELED);
    } else {
      Intent i = new Intent();
      i.putExtra(KEY_NEW_SCORE, newScore);
      i.putExtra(KEY_SUB_BOARD, subBoard);
      i.putExtra(KEY_SIGNATURE, scoreSignature);
      if (name != null && comments != null) {
        i.putExtra(KEY_NAME, name);
        i.putExtra(KEY_COMMENTS, comments);
        i.putExtra(KEY_LOCATION, getLocation());
      }
      setResult(RESULT_OK, i);
    }
  }
  
  private String getLocation() {
    Location location = locator.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
    if (location == null) {
      return "";
    }
    try {
      Address address = geocoder.getFromLocation(location.getLatitude(), location.getLongitude(), 1).get(0);
      if (address == null || address.getAdminArea() == null) {
        return "";
      }
      return address.getAdminArea();
    } catch (IOException e) {
      return "";
    }
  }
  
  private void updateTableWithNewHighScore() {
    // Replace "YOU!" with entered name.
    TableLayout table = (TableLayout) findViewById(R.id.scoreboard);
    if (table == null) return;
    TableRow youRow = (TableRow) table.findViewWithTag(resources.getString(R.string.you));
    if (youRow == null) return;
    setNameDateLocationComments((TextView) youRow.getChildAt(1), name, systemDateFormat
            .format(new Date()).toString(), getLocation(), comments);
    youRow.setTag(null);
  }
  
  /**
   * Updates the current display table with the response.
   */
  private void updateTable() {
    TableLayout table = (TableLayout) findViewById(R.id.scoreboard);
    if (table == null) return;
    table.removeAllViews();

    if (scores == null) {
      TableRow row = new TableRow(this);
      TextView numberView = new TextView(this);
      numberView.setText(resources.getString(R.string.scoreboard_error));
      numberView.setPadding(15, 3, 15, 3);
      numberView.setGravity(Gravity.CENTER);
      row.addView(numberView, new TableRow.LayoutParams(1));
      table.addView(row, new TableLayout.LayoutParams());
      return;
    }

    newPlace = 0;
    for (int i = 0; i < 10; i++) {
      int currScore = 0;
      String name = "Empty";
      String comments = "";
      String created = "";
      String location = "";
      if (i < scores.length()) {
        try {
          JSONObject score = scores.getJSONObject(i);
          currScore = score.getInt(JSON_SCORE);
          name = score.getString(JSON_NAME);
          comments = score.getString(JSON_COMMENTS);
          location = score.optString(JSON_LOCATION);
          created = systemDateFormat.format(1000 * score.getLong(JSON_CREATED))
                  .toString();
        } catch (JSONException e) {
          appendRow(table, i, resources.getString(R.string.score_error), "", "",
                  "", false);
          continue;
        }
      }
      if (newPlace == 0 && newScore >= currScore) {
        appendRow(table, i++, resources.getString(R.string.you),
                systemDateFormat.format(new Date()).toString(), getLocation(), "", newScore,
                true);
        newPlace = i;
      }
      if (i < scores.length()) {
        appendRow(table, i, name, created, location, comments, currScore, false);
      }
    }

    if (newPlace > 0) {
      if (!showedDialog && name == null && comments == null) {
        showedDialog = true;
        name = "";
        comments = "";
        showDialog(0);
      }
    } else if (newScore > 0) {
      appendRow(table, 10, resources.getString(R.string.you),
                systemDateFormat.format(new Date()).toString(),
                getLocation(),
                String.format("Better than %.0f%% of scores",
                              getPercentile(newScore)),
                newScore, true);
    }
  }

  /**
   * Adds a new row to the table.
   * 
   * @param table
   * @param pos
   * @param name
   * @param created
   * @param comments
   * @param selected
   */
  private void appendRow(TableLayout table, int pos, String name,
          String created, String location, String comments, boolean selected) {
    appendRow(table, pos, name, created, location, comments, 0, selected);
  }

  /**
   * Adds a new row to the table.
   * 
   * @param table
   * @param pos
   * @param name
   * @param created
   * @param comments
   * @param score
   * @param selected
   */
  private void appendRow(TableLayout table, int pos, String name,
          String created, String location, String comments, int score, boolean selected) {
    setProgressBarVisibility(false);
    TableRow row = new TableRow(this);
    if (selected) {
      row.setBackgroundColor(Color.argb(0x99, 0xff, 0x00, 0x00));
      row.setTag(resources.getString(R.string.you));
    } else if (pos % 2 == 1) {
      row.setBackgroundColor(Color.argb(0x33, 0x99, 0x99, 0x99));
    }

    // Number
    TextView numberView = new TextView(this);
    if (pos < 10) {
      numberView.setText((pos < 9 ? " " : "") + (pos + 1) + ". ");
    }
    numberView.setTextSize((int) (1.2 * numberView.getTextSize()));
    numberView.setPadding(5, 3, 0, 3);
    row.addView(numberView, new TableRow.LayoutParams(
            ViewGroup.LayoutParams.WRAP_CONTENT,
            ViewGroup.LayoutParams.WRAP_CONTENT));

    // Name, Date, Comments
    TextView nameDateCommentsView = new TextView(this);
    setNameDateLocationComments(nameDateCommentsView, name, created, location, comments);
    row.addView(nameDateCommentsView, new TableRow.LayoutParams());

    // Score
    TextView scoreView = new TextView(this);
    scoreView.setText(score > 0 ? "" + score : "");
    scoreView.setTypeface(scoreView.getTypeface(), Typeface.BOLD);
    scoreView.setPadding(0, 3, 15, 3);
    scoreView.setGravity(Gravity.RIGHT | Gravity.TOP);
    row.addView(scoreView, new TableRow.LayoutParams(
            ViewGroup.LayoutParams.WRAP_CONTENT,
            ViewGroup.LayoutParams.WRAP_CONTENT));

    table.addView(row, new TableLayout.LayoutParams());
    if (selected) {
      // Scroll highlighted cell into view.
      row.setFocusableInTouchMode(true);
      row.requestFocus();
    }
  }

  /**
   * Formats the name, date, and comments for display in the table.
   * 
   * @param view
   * @param name
   * @param created
   * @param comments
   */
  private void setNameDateLocationComments(TextView view, String name, String created, String location,
          String comments) {
    int nameLength = name.length() + 3;
    int createdLocationLength = created.length();
    StringBuffer nameDateComments = new StringBuffer(name);
    nameDateComments.append("   ");
    nameDateComments.append(created);
    if (location != null && location.length() > 0) {
      nameDateComments.append(" - ");
      nameDateComments.append(location);
      createdLocationLength += 3 + location.length();
    }
    if (comments.length() > 0) {
      nameDateComments.append("\n");
      nameDateComments.append(comments);
    }

    view.setText(nameDateComments.toString(), TextView.BufferType.SPANNABLE);
    Spannable str = (Spannable) view.getText();
    str.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0,
            name.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

    str.setSpan(new ForegroundColorSpan(android.graphics.Color.GRAY),
                nameLength, nameLength + createdLocationLength,
            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    view.setPadding(0, 3, 0, 3);
    view.setGravity(Gravity.LEFT | Gravity.TOP);
  }
}